ImagenCUNEF.png

Máster en Data Science para Finanzas¶

Práctica Fraude bancario¶

Contacto:

Gaspar Cólogan Barajas

  • Correo: gaspar.cologan@cunef.edu

Jose Manuel de Castro Beristraín

  • Correo: josemanuel.decastro@cunef.edu

Descripción del problema¶

Este trabajo consiste en analizar y prevenir el fraude bancario en base a una extracción de solicitud de apertura de una cuenta bancaria con un conjunto de datos anonimizados.

El conjunto de datos fue creado por Sérgio Jesus, José Pombal, Duarte Alves, André F. Cruz, Pedro Saleiro y Pedro Bizarro en nombre de Feedzai y fue financiado por la misma empresa. Consiste en seis conjuntos de datos. En esta práctica, utilizaremos unicamente los el conjunto de datos "base" donde no se introdujo sesgo en el proceso de muestreo.

Cada instancia del conjuntos de datos representa una aplicación sintética de la apertura apertura de una cuenta bancaria, generada utilizando un modelo CTGAN entrenado con un conjunto de datos real anonimizado para la detección de fraudes en la apertura de cuentas bancarias. La información original se obtuvo durante el proceso de solicitud con el consentimiento del usuario. No se realizaron revisiones éticas y la recopilación de datos no se realizó directamente de los individuos, sino a través del muestreo de un modelo CTGAN.

El etiquetado se realiza a través del campo "fraud_bool", donde un valor positivo (fraud_bool=1) indica una solicitud fraudulenta y un valor negativo (fraud_bool=0) indica una solicitud legítima. El conjunto de datos aborda la selección de etiquetas y presenta una pequeña selección sesgada debido a restricciones regulatorias y comerciales que limitan el tipo de clientes que algunos bancos pueden aceptar.

Diccionario de datos¶

Diccionario1.png

Diccionario2.png

Importamos librerías¶

In [1]:
import pandas as pd
import numpy as np
import seaborn as sns
from matplotlib import pyplot as plt
import plotly.express as px
import warnings
import category_encoders as ce
from sklearn.preprocessing import OneHotEncoder


warnings.filterwarnings('ignore')


pd.set_option('display.max_columns', 500)
pd.set_option('display.max_rows', 5000)

Importamos funciones definidas dentro un fichero .py¶

In [2]:
def dame_variables_categoricas(dataset=None, max_valores_distintos=100):
    '''
    ----------------------------------------------------------------------------------------------------------
    Función dame_variables_categoricas:
    ----------------------------------------------------------------------------------------------------------
        -Descripción: Función que recibe un dataset y devuelve una lista con los nombres de las 
        variables categóricas
        -Inputs: 
            -- dataset: Pandas dataframe que contiene los datos
            -- max_valores_distintos: Número máximo de valores distintos permitidos para considerar
                una variable como categórica (por defecto 100)
        -Return:
            -- lista_variables_categoricas: lista con los nombres de las variables categóricas del
            dataset de entrada con menos de max_valores_distintos valores diferentes
            -- 1: la ejecución es incorrecta
    '''
    if dataset is None:
        print(u'\nFaltan argumentos por pasar a la función')
        return 1
    
    lista_variables_categoricas = []

    for columna in dataset.columns:
        if pd.api.types.is_object_dtype(dataset[columna]):
            # Si el tipo de dato es 'object' (categórico)
            if len(dataset[columna].dropna().unique()) < max_valores_distintos:
                lista_variables_categoricas.append(columna)

    return lista_variables_categoricas

def plot_feature(df, col_name, isContinuous, target):
    """
    Visualize a variable with and without faceting on the loan status.
    - df dataframe
    - col_name is the variable name in the dataframe
    - full_name is the full variable name
    - continuous is True if the variable is continuous, False otherwise
    """
    f, (ax1, ax2) = plt.subplots(nrows=1, ncols=2, figsize=(12,3), dpi=90)
    
    count_null = df[col_name].isnull().sum()
    if isContinuous:
        
        sns.histplot(df.loc[df[col_name].notnull(), col_name], kde=False, ax=ax1)
    else:
        sns.countplot(df[col_name], order=sorted(df[col_name].unique()), color='#5975A4', saturation=1, ax=ax1)
    ax1.set_xlabel(col_name)
    ax1.set_ylabel('Count')
    ax1.set_title(col_name+ ' Numero de nulos: '+str(count_null))
    plt.xticks(rotation = 90)


    if isContinuous:
        sns.boxplot(x=df[col_name], y=df[target], ax=ax2)
        ax2.set_ylabel('')
        ax2.set_title(col_name + ' by '+target)
    else:
        data = df.groupby(col_name)[target].value_counts(normalize=True).to_frame('proportion').reset_index() 
        data.columns = [i, target, 'proportion']
        #sns.barplot(x = col_name, y = 'proportion', hue= target, data = data, saturation=1, ax=ax2)
        sns.barplot(x = col_name, y = 'proportion', hue= target, data = data, saturation=1, ax=ax2)
        ax2.set_ylabel(target+' fraction')
        ax2.set_title(target)
        plt.xticks(rotation = 90)
    ax2.set_xlabel(col_name)
    
    plt.tight_layout()
    
def get_corr_matrix(dataset = None, metodo='pearson', size_figure=[10,8]):
    # Para obtener la correlación de Spearman, sólo cambiar el metodo por 'spearman'

    if dataset is None:
        print(u'\nHace falta pasar argumentos a la función')
        return 1
    sns.set(style="white")
    # Compute the correlation matrix
    corr = dataset.corr(method=metodo) 
    # Set self-correlation to zero to avoid distraction
    for i in range(corr.shape[0]):
        corr.iloc[i, i] = 0
    # Set up the matplotlib figure
    f, ax = plt.subplots(figsize=size_figure)
    # Draw the heatmap with the mask and correct aspect ratio
    sns.heatmap(corr, center=0,
                square=True, linewidths=.5,  cmap ='viridis' ) #cbar_kws={"shrink": .5}
    plt.show()
    
    return 0

def get_deviation_of_mean_perc(pd_loan, list_var_continuous, target, multiplier):
    pd_final = pd.DataFrame()

    for i in list_var_continuous:
        series_mean = pd_loan[i].mean()
        series_std = pd_loan[i].std()
        std_amp = multiplier * series_std
        left = series_mean - std_amp
        right = series_mean + std_amp
        size_s = pd_loan[i].size

        perc_goods = pd_loan[i][(pd_loan[i] >= left) & (pd_loan[i] <= right)].size / size_s
        perc_excess = pd_loan[i][(pd_loan[i] < left) | (pd_loan[i] > right)].size / size_s

        if perc_excess > 0:
            pd_concat_percent = pd_loan[target][(pd_loan[i] < left) | (pd_loan[i] > right)] \
                .value_counts(normalize=True).reset_index()
            if not pd_concat_percent.empty:
                pd_concat_percent.columns = ['value', 'percentage']
                pd_concat_percent = pd_concat_percent[pd_concat_percent['value'] != 0]
                pd_concat_percent['variable'] = i
                pd_concat_percent['sum_outlier_values'] = pd_loan[i][(pd_loan[i] < left) | (pd_loan[i] > right)].size
                pd_concat_percent['porcentaje_sum_null_values'] = perc_excess
                pd_final = pd.concat([pd_final, pd_concat_percent], axis=0).reset_index(drop=True)

    if pd_final.empty:
        print('No existen variables con valores nulos')

    print()
    return pd_final


def get_percent_null_values_target(pd_loan, list_var_continuous, target):

    pd_final = pd.DataFrame()
    for i in list_var_continuous:
        if pd_loan[i].isnull().sum()>0:
            pd_concat_percent = pd.DataFrame(pd_loan[target][pd_loan[i].isnull()]\
                                            .value_counts(normalize=True).reset_index()).T
            pd_concat_percent.columns = [pd_concat_percent.iloc[0,0], 
                                         pd_concat_percent.iloc[0,1]]
            pd_concat_percent = pd_concat_percent.drop('index',axis=0)
            pd_concat_percent['variable'] = i
            pd_concat_percent['sum_null_values'] = pd_loan[i].isnull().sum()
            pd_concat_percent['porcentaje_sum_null_values'] = pd_loan[i].isnull().sum()/pd_loan.shape[0]
            pd_final = pd.concat([pd_final, pd_concat_percent], axis=0).reset_index(drop=True)
            
    if pd_final.empty:
        print('No existen variables con valores nulos')
        
    return pd_final

def distribucion_categoricas(df, variable, target='fraud_bool'):
    result = df.groupby([variable, target]).size().unstack(fill_value=0)
    result['relative_frequency_0'] = result['0'] / (result['0'] + result['1'])
    result['relative_frequency_1'] = result['1'] / (result['0'] + result['1'])

    # Mostrar el resultado
    display(result)

Cargar y entender los datos¶

In [3]:
df_fraud = pd.read_csv("../data/Base.csv")
df_fraud.head()
Out[3]:
fraud_bool income name_email_similarity prev_address_months_count current_address_months_count customer_age days_since_request intended_balcon_amount payment_type zip_count_4w velocity_6h velocity_24h velocity_4w bank_branch_count_8w date_of_birth_distinct_emails_4w employment_status credit_risk_score email_is_free housing_status phone_home_valid phone_mobile_valid bank_months_count has_other_cards proposed_credit_limit foreign_request source session_length_in_minutes device_os keep_alive_session device_distinct_emails_8w device_fraud_count month
0 1 0.9 0.166828 -1 88 50 0.020925 -1.331345 AA 769 10650.765523 3134.319630 3863.647740 1 6 CA 185 0 BA 1 0 24 0 500.0 0 INTERNET 3.888115 windows 0 1 0 7
1 1 0.9 0.296286 -1 144 50 0.005418 -0.816224 AB 366 534.047319 2670.918292 3124.298166 718 3 CA 259 1 BA 0 0 15 0 1500.0 0 INTERNET 31.798819 windows 0 1 0 7
2 1 0.9 0.044985 -1 132 40 3.108549 -0.755728 AC 870 4048.534263 2893.621498 3159.590679 1 14 CB 177 1 BA 0 1 -1 0 200.0 0 INTERNET 4.728705 other 0 1 0 7
3 1 0.9 0.159511 -1 22 50 0.019079 -1.205124 AB 810 3457.064063 4054.908412 3022.261812 1921 6 CA 110 1 BA 0 1 31 1 200.0 0 INTERNET 2.047904 linux 0 1 0 7
4 1 0.9 0.596414 -1 218 50 0.004441 -0.773276 AB 890 5020.341679 2728.237159 3087.670952 1990 2 CA 295 1 BA 1 0 31 0 1500.0 0 INTERNET 3.775225 macintosh 1 1 0 7

En primer lugar, leemos nuestro archivo CSV y hacemos un .head con el fin de tener una visión global de nuestro dataframe. Es cierto que no obtenemos ninguna información relevante ni adicional de los datos, no obstante, nos permite tener una idea general de los datos con los que estamos tratando.

Análisis de datos exploratorio - EDA¶

In [4]:
df_fraud.columns
Out[4]:
Index(['fraud_bool', 'income', 'name_email_similarity',
       'prev_address_months_count', 'current_address_months_count',
       'customer_age', 'days_since_request', 'intended_balcon_amount',
       'payment_type', 'zip_count_4w', 'velocity_6h', 'velocity_24h',
       'velocity_4w', 'bank_branch_count_8w',
       'date_of_birth_distinct_emails_4w', 'employment_status',
       'credit_risk_score', 'email_is_free', 'housing_status',
       'phone_home_valid', 'phone_mobile_valid', 'bank_months_count',
       'has_other_cards', 'proposed_credit_limit', 'foreign_request', 'source',
       'session_length_in_minutes', 'device_os', 'keep_alive_session',
       'device_distinct_emails_8w', 'device_fraud_count', 'month'],
      dtype='object')
In [5]:
df_fraud.dtypes.sort_values().to_frame('feature_type').groupby(by = 'feature_type').size().to_frame('count').reset_index()
Out[5]:
feature_type count
0 int64 18
1 float64 9
2 object 5
In [6]:
dimension=df_fraud.shape, df_fraud.drop_duplicates().shape
dimension
Out[6]:
((1000000, 32), (1000000, 32))

Podemos observar que nuestro dataframe contiene 1.000.000 de instancias distribuidas en 32 columnas. Estas columnas, son principalmente de tipo int64 ya que 18 son de este tipo, 9 son float64 y 5 tipo object.

In [7]:
df_fraud
Out[7]:
fraud_bool income name_email_similarity prev_address_months_count current_address_months_count customer_age days_since_request intended_balcon_amount payment_type zip_count_4w velocity_6h velocity_24h velocity_4w bank_branch_count_8w date_of_birth_distinct_emails_4w employment_status credit_risk_score email_is_free housing_status phone_home_valid phone_mobile_valid bank_months_count has_other_cards proposed_credit_limit foreign_request source session_length_in_minutes device_os keep_alive_session device_distinct_emails_8w device_fraud_count month
0 1 0.9 0.166828 -1 88 50 0.020925 -1.331345 AA 769 10650.765523 3134.319630 3863.647740 1 6 CA 185 0 BA 1 0 24 0 500.0 0 INTERNET 3.888115 windows 0 1 0 7
1 1 0.9 0.296286 -1 144 50 0.005418 -0.816224 AB 366 534.047319 2670.918292 3124.298166 718 3 CA 259 1 BA 0 0 15 0 1500.0 0 INTERNET 31.798819 windows 0 1 0 7
2 1 0.9 0.044985 -1 132 40 3.108549 -0.755728 AC 870 4048.534263 2893.621498 3159.590679 1 14 CB 177 1 BA 0 1 -1 0 200.0 0 INTERNET 4.728705 other 0 1 0 7
3 1 0.9 0.159511 -1 22 50 0.019079 -1.205124 AB 810 3457.064063 4054.908412 3022.261812 1921 6 CA 110 1 BA 0 1 31 1 200.0 0 INTERNET 2.047904 linux 0 1 0 7
4 1 0.9 0.596414 -1 218 50 0.004441 -0.773276 AB 890 5020.341679 2728.237159 3087.670952 1990 2 CA 295 1 BA 1 0 31 0 1500.0 0 INTERNET 3.775225 macintosh 1 1 0 7
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
999995 0 0.6 0.192631 -1 104 40 0.030592 -1.044454 AB 804 7905.711839 8341.468557 4972.635997 1 8 CA 75 1 BC 1 1 25 0 200.0 0 INTERNET 8.511502 linux 1 1 0 4
999996 0 0.8 0.322989 148 9 50 1.628119 -1.409803 AC 3306 5391.470463 4955.170808 5022.728108 0 2 CC 154 1 BC 1 1 -1 0 200.0 0 INTERNET 8.967865 windows 0 1 0 4
999997 0 0.8 0.879403 -1 30 20 0.018563 34.692760 AA 1522 8063.102636 5670.654316 4377.196321 2023 6 CF 64 0 BC 0 1 11 0 200.0 0 INTERNET 8.195531 other 0 1 0 4
999998 0 0.9 0.762112 -1 189 20 0.015352 94.661055 AA 1418 8092.641762 3982.582204 4394.803296 1678 6 CA 163 0 BA 1 0 28 0 500.0 0 INTERNET 4.336064 windows 1 1 0 4
999999 0 0.2 0.697452 -1 321 20 2.655916 9.908499 AA 951 6169.630036 3695.308261 4352.334543 2 12 CA 36 1 BE 0 1 15 0 200.0 0 INTERNET 6.717022 linux 0 1 0 4

1000000 rows × 32 columns

In [8]:
df_fraud.describe()
Out[8]:
fraud_bool income name_email_similarity prev_address_months_count current_address_months_count customer_age days_since_request intended_balcon_amount zip_count_4w velocity_6h velocity_24h velocity_4w bank_branch_count_8w date_of_birth_distinct_emails_4w credit_risk_score email_is_free phone_home_valid phone_mobile_valid bank_months_count has_other_cards proposed_credit_limit foreign_request session_length_in_minutes keep_alive_session device_distinct_emails_8w device_fraud_count month
count 1000000.000000 1000000.000000 1000000.000000 1000000.000000 1000000.000000 1000000.000000 1.000000e+06 1000000.000000 1000000.000000 1000000.000000 1000000.000000 1000000.000000 1000000.000000 1000000.000000 1000000.000000 1000000.000000 1000000.000000 1000000.000000 1000000.000000 1000000.000000 1000000.000000 1000000.000000 1000000.000000 1000000.000000 1000000.000000 1000000.0 1000000.000000
mean 0.011029 0.562696 0.493694 16.718568 86.587867 33.689080 1.025705e+00 8.661499 1572.692049 5665.296605 4769.781965 4856.324016 184.361849 9.503544 130.989595 0.529886 0.417077 0.889676 10.839303 0.222988 515.851010 0.025242 7.544940 0.576947 1.018312 0.0 3.288674
std 0.104438 0.290343 0.289125 44.046230 88.406599 12.025799 5.381835e+00 20.236155 1005.374565 3009.380665 1479.212612 919.843934 459.625329 5.033792 69.681812 0.499106 0.493076 0.313293 12.116875 0.416251 487.559902 0.156859 8.033106 0.494044 0.180761 0.0 2.209994
min 0.000000 0.100000 0.000001 -1.000000 -1.000000 10.000000 4.036860e-09 -15.530555 1.000000 -170.603072 1300.307314 2825.748405 0.000000 0.000000 -170.000000 0.000000 0.000000 0.000000 -1.000000 0.000000 190.000000 0.000000 -1.000000 0.000000 -1.000000 0.0 0.000000
25% 0.000000 0.300000 0.225216 -1.000000 19.000000 20.000000 7.193246e-03 -1.181488 894.000000 3436.365848 3593.179135 4268.368423 1.000000 6.000000 83.000000 0.000000 0.000000 1.000000 -1.000000 0.000000 200.000000 0.000000 3.103053 0.000000 1.000000 0.0 1.000000
50% 0.000000 0.600000 0.492153 -1.000000 52.000000 30.000000 1.517574e-02 -0.830507 1263.000000 5319.769349 4749.921161 4913.436941 9.000000 9.000000 122.000000 1.000000 0.000000 1.000000 5.000000 0.000000 200.000000 0.000000 5.114321 1.000000 1.000000 0.0 3.000000
75% 0.000000 0.800000 0.755567 12.000000 130.000000 40.000000 2.633069e-02 4.984176 1944.000000 7680.717827 5752.574191 5488.083356 25.000000 13.000000 178.000000 1.000000 1.000000 1.000000 25.000000 0.000000 500.000000 0.000000 8.866131 1.000000 1.000000 0.0 5.000000
max 1.000000 0.900000 0.999999 383.000000 428.000000 90.000000 7.845690e+01 112.956928 6700.000000 16715.565404 9506.896596 6994.764201 2385.000000 39.000000 389.000000 1.000000 1.000000 1.000000 32.000000 1.000000 2100.000000 1.000000 85.899143 1.000000 2.000000 0.0 7.000000

El conjunto de datos comprende un total de 1,000,000 registros, cada uno caracterizando diversas aplicaciones bancarias. Una de las variables clave es "fraud_bool", una variable binaria que indica si una aplicación es fraudulenta o no. En promedio, alrededor del 1.10% de las aplicaciones en el conjunto de datos se clasifican como fraudulentas.

En cuanto a los perfiles de los solicitantes, la variable "income" revela que los ingresos anuales, expresados en deciles, tienen una media de aproximadamente 0.56. Esto sugiere una distribución variada de ingresos en la población de solicitantes. Además, la métrica "name_email_similarity" proporciona un valor promedio de alrededor del 0.49, indicando la similitud entre el nombre y el correo electrónico del solicitante.

Explorando los detalles de las direcciones, la variable "prev_address_months_count" indica que, en promedio, los solicitantes han residido alrededor de 16.72 meses en su dirección anterior, con una considerable variabilidad representada por una desviación estándar de 44.05 meses. Por otro lado, la variable "current_address_months_count" muestra que, en promedio, los solicitantes han vivido aproximadamente 86.59 meses en su dirección actual, con una desviación estándar de 88.41 meses.

La edad media de los solicitantes en el conjunto de datos es de aproximadamente 33.69 años, redondeada a la década más cercana. Este dato sugiere que la población de solicitantes es relativamente joven. La edad mínima es de 10 años, y la máxima es de 90 años, reflejando una amplia diversidad en la edad de los solicitantes.

Por último, el número de Aplicaciones Fraudulentas con el Mismo Dispositivo (device_fraud_count), Con una media de aproximadamente 0.00, esta variable indica la frecuencia promedio de aplicaciones fraudulentas asociadas al mismo dispositivo en las últimas 8 semanas. La mayoría de las aplicaciones no parecen tener asociaciones fraudulentas con el dispositivo utilizado.

Variable objetivo "fruad_bool":¶

In [9]:
df_fraud_fraud_bool = df_fraud['fraud_bool']\
        .value_counts(normalize=True)\
        .mul(100).rename('percent').reset_index()

df_fraud_fraud_bool_conteo = df_fraud['fraud_bool'].value_counts().reset_index()
df_fraud_fraud_bool_pc = pd.merge(df_fraud_fraud_bool, df_fraud_fraud_bool_conteo, how='inner')
df_fraud_fraud_bool_pc
Out[9]:
fraud_bool percent count
0 0 98.8971 988971
1 1 1.1029 11029
In [10]:
import plotly.express as px

# Suponiendo que 'index' es una columna en df_fraud_fraud_bool_pc
fig = px.histogram(df_fraud_fraud_bool_pc, x=df_fraud_fraud_bool_pc.index, y=['percent'])
fig.show()

La variable objetivo "fraud_bool", representa con 1 los casos donde ha habido fraude bancario y con 0 representa los casos que son legítimos. En este dataframe, nos encontramos con un 1,10 % de casos fraudulentos, lo que representan aproximadamente 11.000 instancias del millón que posee.

Nulos¶

Tal y como se indicó en el diccionario de datos, algunas variables contienen nulos que están definidos con valores negativos. A continuación, vamos a analizar la cantidad de datos negativos que poseen cada una de estas columnas y analizaremos si es relevante mantener esta información o es favorable omitirla.

In [11]:
# Lista de columnas con valores negativos o -1
columns_with_negatives = [
    'prev_address_months_count',
    'current_address_months_count',
    'intended_balcon_amount',
    'bank_months_count',
    'session_length_in_minutes',
    'device_distinct_emails_8w'
]

# Filtrar el DataFrame para incluir solo las columnas de interés
df_negatives = df_fraud[columns_with_negatives]

# Convertir columnas a números si es posible
df_negatives = df_negatives.apply(pd.to_numeric, errors='coerce')

# Contar los valores negativos o iguales a -1 por cada columna
negatives_count_per_column = df_negatives.lt(0).sum()

# Calcular el porcentaje de valores negativos en cada columna
total_values_per_column = df_negatives.count()
percent_negatives_per_column = (negatives_count_per_column / total_values_per_column)

# Crear DataFrame con los resultados
result_df = pd.DataFrame({
    'negativos_columnas': negatives_count_per_column,
    'porcentaje_negativos_columnas': percent_negatives_per_column
})

# Ordenar el DataFrame por el porcentaje de mayor a menor
result_df = result_df.sort_values(by='porcentaje_negativos_columnas', ascending=False)

# Imprimir el resultado
display(result_df)
negativos_columnas porcentaje_negativos_columnas
intended_balcon_amount 742523 0.742523
prev_address_months_count 712920 0.712920
bank_months_count 253635 0.253635
current_address_months_count 4254 0.004254
session_length_in_minutes 2015 0.002015
device_distinct_emails_8w 359 0.000359

Observamos que 2 de nuestras variables intended_balcon_amount y prev_address_months_count tienen un alto porcentaje de valores faltantes, en ambos casos supera el 70%. En el caso de la columna bank_months_count un cuarto de los valores son nulos. Y encontramos también otras tres variables con uno número bastante escaso de valores nulos.

Análisis de las columnas con nulos con respecto a la columna Fraud Bool¶

A continuación, vamos a hacer una análisis más exhaustivo de estos valores nulos para determinar si podemos eliminar estos registros. En concreto, vamos a ver si hay algún valor añadido que podamos concluir a raíz de un valor nulo, centrandonos en la relación nulo con el fraude bancario.

In [12]:
# Lista de columnas con valores negativos o -1
columns_with_negatives = [
    'prev_address_months_count',
    'current_address_months_count',
    'intended_balcon_amount',
    'bank_months_count',
    'session_length_in_minutes',
    'device_distinct_emails_8w'
]

# Filtrar el DataFrame para incluir solo las columnas de interés
df_negatives = df_fraud[columns_with_negatives + ['fraud_bool']]

# Convertir columnas a números si es posible
df_negatives[columns_with_negatives] = df_negatives[columns_with_negatives].apply(pd.to_numeric, errors='coerce')

# Contar los valores nulos (menores a 0):
negatives_count_per_column = df_negatives[columns_with_negatives].lt(0).sum()
positive_count_per_column = df_negatives[columns_with_negatives].ge(0).sum()

# Crear DataFrame con los resultados
result_df = pd.DataFrame({
    'columna': columns_with_negatives,
    'total_nulos': negatives_count_per_column,
    'nulos_0': df_negatives[df_negatives['fraud_bool'] == 0][columns_with_negatives].lt(0).sum(),
    'nulos_1': df_negatives[df_negatives['fraud_bool'] == 1][columns_with_negatives].lt(0).sum(),
    'total_no_nulos': positive_count_per_column,
    'no_nulos_0': df_negatives[df_negatives['fraud_bool'] == 0][columns_with_negatives].ge(0).sum(),
    'no_nulos_1': df_negatives[df_negatives['fraud_bool'] == 1][columns_with_negatives].ge(0).sum(),
})

# Calcular porcentajes de valores nulos
result_df['porcentaje_nulos_0'] = (result_df['nulos_0'] / result_df['total_nulos']) * 100
result_df['porcentaje_nulos_1'] = (result_df['nulos_1'] / result_df['total_nulos']) * 100

# Calcular porcentajes de valores no nulos
result_df['porcentaje_no_nulos_0'] = (result_df['no_nulos_0'] / (result_df['no_nulos_0'] + result_df['no_nulos_1'])) * 100
result_df['porcentaje_no_nulos_1'] = (result_df['no_nulos_1'] / (result_df['no_nulos_0'] + result_df['no_nulos_1'])) * 100

# Reiniciar el índice para evitar duplicados
result_df.reset_index(drop=True, inplace=True)

# Reorganizar las columnas para tener una estructura más clara
result_df = result_df[['columna', 'total_nulos', 'nulos_0', 'nulos_1',
                       'porcentaje_nulos_0', 'porcentaje_nulos_1', 'total_no_nulos',
                       'no_nulos_0', 'no_nulos_1',
                       'porcentaje_no_nulos_0', 'porcentaje_no_nulos_1']]

# Imprimir el resultado
display(result_df)
columna total_nulos nulos_0 nulos_1 porcentaje_nulos_0 porcentaje_nulos_1 total_no_nulos no_nulos_0 no_nulos_1 porcentaje_no_nulos_0 porcentaje_no_nulos_1
0 prev_address_months_count 712920 702786 10134 98.578522 1.421478 287080 286185 895 99.688240 0.311760
1 current_address_months_count 4254 4240 14 99.670898 0.329102 995746 984731 11015 98.893794 1.106206
2 intended_balcon_amount 742523 732776 9747 98.687313 1.312687 257477 256195 1282 99.502091 0.497909
3 bank_months_count 253635 249495 4140 98.367733 1.632267 746365 739476 6889 99.076993 0.923007
4 session_length_in_minutes 2015 1997 18 99.106700 0.893300 997985 986974 11011 98.896677 1.103323
5 device_distinct_emails_8w 359 355 4 98.885794 1.114206 999641 988616 11025 98.897104 1.102896

La tabla anterior muestra con valores absolutos y porcentuales cuantos de nuestros valores nulos conllevan fraude bancario y cuantos no. Gracias a un análisis previo, determinamos que aproximadamente 1,10 % de nuestro dataset era fraude bancario y el 98,9 % es legítimo, por lo que en cuanto más próximo se encuentre la distribución del fraude individualmente en cada variable, significa que no nos aporta ningún valor añadido los números nulos.

La variable "intended_balcon_amount" es la que mayor representación de valores nulos tiene. Vemos que el 98,69% de los valores nulos no conllevan fraude bancario, y por ende, el 1,31% de los nulos si son fraude bancario. En el caso de los valores no nulos, 99.50 % de los datos no son fraude bancario y aproximadamente el 0,5% de los datos sí son fraude. Es decir, En caso de que tengamos un valor nulo dentro de la variable "intended_balcon_amount", hay más del doble de posibilidad que nos encontremos ante un caso de fraude bancario.

En caso de nuestra segunda variable con más nulos, "prev_address_months_count", observamos que el 1,42% de nuestros valores nulos son fraude bancario. Este porcentaje es aproximadamente 4 veces más grande que el que nos encontramos en el caso de los valores no nulos, el cual posee un 0,31% de fraude bancario.

En la columna llamada "bank_months_count", también observamos un porcentaje de fraude superior para los datos nulos (1,63%), que en caso de los datos sin nulos, donde hay un 0,92% de casos fraudulentos.

Por lo tanto, podemos concluir que estos valores nulos nos aportan un gran valor añadido a nuestro dataframe ya que en las variables con un alto índice de datos faltantes, la probabilidad de encontrarnos con fraude es superior a la distribución global de nuestro dataframe (aproximadamente 1,10%), y es más probable encontrarnos con fraude bancario en caso de tener un valor nulo que en caso de no tenerlo. Por lo tanto, hemos decidido mantener estos datos en nuestro dataframe y no eliminarlos. El resto de las variables que poseen algún dato nulo tampoco van a ser manipuladas ya que su representación es escasa e indiferente para nuestro análisis.

Representación gráfica de las variables numéricas con respecto a nuestra columna objetivo:¶

Tipos: Variables categoricas y numericas¶

In [13]:
df_fraud.dtypes
Out[13]:
fraud_bool                            int64
income                              float64
name_email_similarity               float64
prev_address_months_count             int64
current_address_months_count          int64
customer_age                          int64
days_since_request                  float64
intended_balcon_amount              float64
payment_type                         object
zip_count_4w                          int64
velocity_6h                         float64
velocity_24h                        float64
velocity_4w                         float64
bank_branch_count_8w                  int64
date_of_birth_distinct_emails_4w      int64
employment_status                    object
credit_risk_score                     int64
email_is_free                         int64
housing_status                       object
phone_home_valid                      int64
phone_mobile_valid                    int64
bank_months_count                     int64
has_other_cards                       int64
proposed_credit_limit               float64
foreign_request                       int64
source                               object
session_length_in_minutes           float64
device_os                            object
keep_alive_session                    int64
device_distinct_emails_8w             int64
device_fraud_count                    int64
month                                 int64
dtype: object
In [14]:
list_var_cat = dame_variables_categoricas(dataset=df_fraud)
df_fraud[list_var_cat] = df_fraud[list_var_cat].astype("category")
list_var_continuous = list(df_fraud.select_dtypes('float').columns)
df_fraud[list_var_continuous] = df_fraud[list_var_continuous].astype(float)
df_fraud.dtypes
Out[14]:
fraud_bool                             int64
income                               float64
name_email_similarity                float64
prev_address_months_count              int64
current_address_months_count           int64
customer_age                           int64
days_since_request                   float64
intended_balcon_amount               float64
payment_type                        category
zip_count_4w                           int64
velocity_6h                          float64
velocity_24h                         float64
velocity_4w                          float64
bank_branch_count_8w                   int64
date_of_birth_distinct_emails_4w       int64
employment_status                   category
credit_risk_score                      int64
email_is_free                          int64
housing_status                      category
phone_home_valid                       int64
phone_mobile_valid                     int64
bank_months_count                      int64
has_other_cards                        int64
proposed_credit_limit                float64
foreign_request                        int64
source                              category
session_length_in_minutes            float64
device_os                           category
keep_alive_session                     int64
device_distinct_emails_8w              int64
device_fraud_count                     int64
month                                  int64
dtype: object

Nuestra variable objetivo la podemos en tipo "string":

In [15]:
df_fraud['fraud_bool'] = df_fraud['fraud_bool'].astype(str)

Seleccionamos unicamente las columnas de tipo númericas para la representación gráfica:

In [16]:
numeric_columns = df_fraud.select_dtypes(include=['float64', 'int64']).columns

# Crear un nuevo DataFrame con las variables numéricas
df_selected = df_fraud[numeric_columns]
In [17]:
distribucion_categoricas(df_fraud, 'payment_type')
distribucion_categoricas(df_fraud, 'employment_status')
distribucion_categoricas(df_fraud, 'housing_status')
distribucion_categoricas(df_fraud, 'source')
distribucion_categoricas(df_fraud, 'device_os')
fraud_bool 0 1 relative_frequency_0 relative_frequency_1
payment_type
AA 256885 1364 0.994718 0.005282
AB 366385 4169 0.988749 0.011251
AC 247862 4209 0.983302 0.016698
AD 117551 1286 0.989178 0.010822
AE 288 1 0.996540 0.003460
fraud_bool 0 1 relative_frequency_0 relative_frequency_1
employment_status
CA 721353 8899 0.987814 0.012186
CB 137335 953 0.993109 0.006891
CC 36826 932 0.975316 0.024684
CD 26422 100 0.996230 0.003770
CE 22640 53 0.997664 0.002336
CF 43949 85 0.998070 0.001930
CG 446 7 0.984547 0.015453
fraud_bool 0 1 relative_frequency_0 relative_frequency_1
housing_status
BA 163318 6357 0.962534 0.037466
BB 259397 1568 0.993992 0.006008
BC 369855 2288 0.993852 0.006148
BD 25935 226 0.991361 0.008639
BE 168553 582 0.996559 0.003441
BF 1662 7 0.995806 0.004194
BG 251 1 0.996032 0.003968
fraud_bool 0 1 relative_frequency_0 relative_frequency_1
source
INTERNET 982035 10917 0.989006 0.010994
TELEAPP 6936 112 0.984109 0.015891
fraud_bool 0 1 relative_frequency_0 relative_frequency_1
device_os
linux 330997 1715 0.994845 0.005155
macintosh 53074 752 0.986029 0.013971
other 340754 1974 0.994240 0.005760
windows 256999 6507 0.975306 0.024694
x11 7147 81 0.988794 0.011206

Las tablas anteriores nos permiten ver como se distribuye el fraude dentro de las variables que son tipo "category".

  • Tabla "payment_type": La frecuencia relativa de fraude es mayor en los pagos de tipo "AE" (0.34%) en comparación con otros tipos de pago.
  • Tabla "employment_status": La frecuencia relativa de fraude es mayor en los casos de "CC" (2.47%) y "CG" (1.55%), que representan ciertos estados laborales.
  • Tabla "housing_status": Los casos de fraude son más frecuentes en las categorías "BA" (3.75%) y "BB" (0.60%).
  • Tabla "source": El fraude es más frecuente en las transacciones originadas por "TELEAPP" (1.59%) en comparación con "INTERNET" (1.10%).
  • Tabla "device_os": El fraude es más común en dispositivos con sistemas operativos "windows" (2.47%) y menos común en "linux" (0.52%).
In [18]:
import time

start_time = time.time()

target = 'fraud_bool'  # Define tu columna objetivo aquí

for col_name in df_selected.columns:
    print(col_name)
    plot_feature(df_fraud, col_name=col_name, isContinuous=True, target=target)
    
end_time = time.time()
elapsed_time = end_time - start_time

print(f'Tiempo total de ejecución: {elapsed_time} segundos')
income
name_email_similarity
prev_address_months_count
current_address_months_count
customer_age
days_since_request
intended_balcon_amount
zip_count_4w
velocity_6h
velocity_24h
velocity_4w
bank_branch_count_8w
date_of_birth_distinct_emails_4w
credit_risk_score
email_is_free
phone_home_valid
phone_mobile_valid
bank_months_count
has_other_cards
proposed_credit_limit
foreign_request
session_length_in_minutes
keep_alive_session
device_distinct_emails_8w
device_fraud_count
month
Tiempo total de ejecución: 145.17566680908203 segundos

Los gráficos anteriores realizados a partir de todas las variables numéricas representan la distribución individual de cada variable y la distribución con respecto a nuestra variable objetivo "fraud_bool". A continuación, vamos a comentar cada uno de estos gráficos:

  • Income: Observamos que los valores se concentran en los extremos de esta variable ya que tenemos muchos valores cercanos al 0.1 y también al 0.8 y 0.9. Al compararlo con la variable objetivo, detectamos que es más probable que se comenta fraude cuando tienes un "income" superior ya que el primer cuartil de ingreso de los que comentes fraude es dos veces superior al que no comete fraude y es igual a la mediana de los que no cometen fraude.
  • name_email_similarity: La distribución de los datos dentro de esta variable es muy equitativa salvo cuando es muy próxima a 1, donde encontramos la moda. Detectamos que cuanto menos índice encontremos dentro de esta variable es más probable que se cometa fraude bancario.
  • prev_address_months_count: Se trata de la columna con un número elevado de datos negativos y solo disponemos del 28,7 % de los datos. Estos son muy limitados y en el gráfico de barras no destacan, pero se encuentran principalmente entre 0 y 50. Para aquellas personas que comenten fraude bancario, es muy común que tengan un 0 dentro de esta variable ya que hasta los datos del tercer cuartil se concentra aquí.
  • current_address_months_count: En el primer gráfico observamos que la mayoría de los datos poseen un valor comprendido entre 0 y 50 y al clasificarlo por fraude o no, detectamos que cuanto inferior sea este variable, menos probable es de cometer fraude bancario. El primer cuartil de los que cometen fraude corresponde con la mediana de los que no comenten y el tercer cuartil es superior al de los ceros.
  • customer_age: La edad de las personas está que solicitan una cuenta bancaria está comprendida entre 10 y 70 años, siendo los 30 años la edad más frecuente. Comprobamos que se comete más fraude a una edad superior. Las personas que cometen fraude tienen frecuentemente de 30 a 50 años.
  • days_since_request: Los datos de esta variable son insignificantes para interpretación ya que son principalmente ceros e indiferentes para distinguir entre las personas que cometen fraude bancario.
  • intended_balcon_amount: Se trata de la variable con más números nulos. Estos nulos están reflejados con -1 en el gráfico y podemos concluir que las personas que comenten fraude bancario no suelen proporcionar este dato y poseen un valor negativo dentro de esta variable.
  • zip_count_4w: Los datos que obtenemos de esta variable son insuficientes para llegar a conclusiones relevantes. No obstante, el primer cuartil, mediana y tercer cuartil es levemente superior para las personas que no cometen fraude que las personas que sí lo comentes.
  • velocity_6h: Los datos que obtenemos de esta variable son insuficientes para llegar a conclusiones relevantes. No obstante, el primer cuartil, mediana y tercer cuartil es levemente superior para las personas que no cometen fraude que las personas que sí lo comentes.
  • velocity_24h: Los datos que obtenemos de esta variable son insuficientes para llegar a conclusiones relevantes. No obstante, el primer cuartil, mediana y tercer cuartil es levemente superior para las personas que no cometen fraude que las personas que sí lo comentes.
  • velocity_4w: Los datos que obtenemos de esta variable son insuficientes para llegar a conclusiones relevantes. No obstante, el primer cuartil, mediana y tercer cuartil es levemente superior para las personas que no cometen fraude que las personas que sí lo comentes.
  • bank_branch_count_8w: Los datos que obtenemos de esta variable son insuficientes para llegar a conclusiones relevantes.
  • date_of_birth_distinct_emails_4w: Los datos de esta variable se comprenden entre 0 y 25, donde los más frecuentes son de 5 a 10 aproximadamente. Al clasificarlos en función de la variable objetivo, vemos que el primer cuartil, mediana y tercer cuartil de las personas que comenten fraude es aproximadamente, 4, 5 y 10 respectivamente. Mientras que el del gráfico de las personas que no comenten es de 6, 9 y 13. Por lo que cuanto mayor sea, menor probabilidad de cometer fraude hay.
  • credit_risk_score: Observamos que cuanto mayor es el número de riesgo, mayor posibilidad hay de que comentan fraude bancario. El valor del primer cuartil de las personas que cometen fraude es levemente inferior a la mediana de los que no cometen y la mediana de los que sí cometen es similar al tercer cuartil de los que no.
  • email_is_free: Los valores se distribuyen entre 0 o 1, y no tienen ninguna característica que permita llegar a alguna conclusión relevante.
  • phone_home_valid: Los valores se distribuyen entre 0 o 1, y no tienen ninguna característica que permita llegar a alguna conclusión relevante.
  • phone_mobile_valid: Los valores se distribuyen entre 0 o 1, y no tienen ninguna característica que permita llegar a alguna conclusión relevante.
  • bank_months_count: Los valores de esta variable se distribuyen de forma similar para las personas que cometen fraude y que no comenten. No obstante, se puede observar que la mediana es algo superior (aproximadamente 2 valores) para las personas lícitas.
  • has_other_cards: Los valores se distribuyen entre 0 o 1, y no tienen ninguna característica que permita llegar a alguna conclusión relevante.
  • proposed_credit_limit: Los valores de esta variable se clasifican de 0 a 2000, sin embargo, podemos observar que si el límite solicitado de superior a 500, es muy probable que se cometa fraude bancario.
  • foreign_request: Los valores se distribuyen entre 0 o 1, y no tienen ninguna característica que permita llegar a alguna conclusión relevante.
  • session_length_in_minutes: Los datos que obtenemos de esta variable son insuficientes para llegar a conclusiones relevantes.
  • keep_alive_session: Los datos que obtenemos de esta variable son insuficientes para llegar a conclusiones relevantes.
  • device_distinct_emails_8w: Los valores se distribuyen entre 0 o 1, y no tienen ninguna característica que permita llegar a alguna conclusión relevante.
  • device_fraud_count: Todos los datos de esta variable son 0, por lo que no nos permite llegar a alguna conclusión relevante.
  • month: El dataset posee datos de únicamente 8 meses, por lo que no tenemos esta variable completa. Sin embargo, observamos que las personas que solicitaron cuenta bancaria a partir del mes 6 (5 según el gráfico), fueron más propensas a cometer fraude bancario.
In [19]:
#Revertimos el cambio y volvemos a poner fraud_bool como int64
df_fraud['fraud_bool'] = df_fraud['fraud_bool'].astype(np.int64)

Tratamiento de outliers¶

Vamos a analizar los valores atípicos con el fin de determinar si los eliminamos o mantenemos en nuestro conjunto de datos:

In [20]:
list_var_continuous = list(df_fraud.select_dtypes('float').columns)
get_deviation_of_mean_perc(df_fraud, list_var_continuous, target='fraud_bool', multiplier=3)

Out[20]:
value percentage variable sum_outlier_values porcentaje_sum_null_values
0 1 0.011758 days_since_request 17775 0.017775
1 1 0.009705 intended_balcon_amount 18960 0.018960
2 1 0.006450 velocity_6h 4341 0.004341
3 1 0.003711 velocity_24h 539 0.000539
4 1 0.129651 proposed_credit_limit 6155 0.006155
5 1 0.020048 session_length_in_minutes 23593 0.023593

Identificamos los valores atípicos (outliers) en variables continuas en un conjunto de datos, y luego calcular la proporción de casos de fraude (fraud_bool = 1) dentro de esos valores atípicos. Vamos a analizar los resultados obtenidos:

Variable 'days_since_request':

  • El 1.18% de los valores en 'days_since_request' están fuera de la media más/menos 3 desviaciones estándar.
  • La proporción de casos de fraude en estos valores atípicos es del 1.78%.
  • Se identificaron 17,775 casos como atípicos.

Variable 'intended_balcon_amount':

  • El 0.97% de los valores en 'intended_balcon_amount' están fuera de la media más/menos 3 desviaciones estándar.
  • La proporción de casos de fraude en estos valores atípicos es del 0.97%.
  • Se identificaron 18,960 casos como atípicos.

Variable 'velocity_6h':

  • El 0.65% de los valores en 'velocity_6h' están fuera de la media más/menos 3 desviaciones estándar.
  • La proporción de casos de fraude en estos valores atípicos es del 0.65%.
  • Se identificaron 4,341 casos como atípicos.

Variable 'velocity_24h':

  • El 0.37% de los valores en 'velocity_24h' están fuera de la media más/menos 3 desviaciones estándar.
  • La proporción de casos de fraude en estos valores atípicos es del 0.05%.
  • Se identificaron 539 casos como atípicos.

Variable 'proposed_credit_limit':

  • El 12.97% de los valores en 'proposed_credit_limit' están fuera de la media más/menos 3 desviaciones estándar.
  • La proporción de casos de fraude en estos valores atípicos es del 6.16%.
  • Se identificaron 6,155 casos como atípicos.

Variable 'session_length_in_minutes':

  • El 2.00% de los valores en 'session_length_in_minutes' están fuera de la media más/menos 3 desviaciones estándar.
  • La proporción de casos de fraude en estos valores atípicos es del 2.36%.
  • Se identificaron 23,593 casos como atípicos.

Finalmente, hemos decidido mantener los outliers dentro de nuestro dataset debido al bajo porcentaje que representan.

Correlaciones¶

Por último, analizamos las correlaciones entre variables continuas en tu conjunto de datos:

In [21]:
get_corr_matrix(dataset = df_fraud[list_var_continuous], 
                metodo='pearson', size_figure=[10,8])
Out[21]:
0
In [22]:
corr = df_fraud[list_var_continuous].corr('pearson')
new_corr = corr.abs()
new_corr.loc[:,:] = np.tril(new_corr, k=-1) 
new_corr = new_corr.stack().to_frame('correlation').reset_index().sort_values(by='correlation', ascending=False)
new_corr[new_corr['correlation']>0.4]
Out[22]:
level_0 level_1 correlation
59 velocity_4w velocity_24h 0.539115
49 velocity_24h velocity_6h 0.464003
58 velocity_4w velocity_6h 0.400254

Observamos una correlación positiva entre las diferentes variables de "velocity":

Correlación entre 'velocity_4w' y 'velocity_24h':

  • Coeficiente de correlación: 0.539115
  • Existe una correlación positiva moderadamente fuerte (0.54) entre la variable 'velocity_4w' y 'velocity_24h'

Correlación entre 'velocity_24h' y 'velocity_6h':

  • Coeficiente de correlación: 0.464003
  • Hay una correlación positiva significativa (0.46) entre 'velocity_24h' y 'velocity_6h'.

Correlación entre 'velocity_4w' y 'velocity_6h':

  • Coeficiente de correlación: 0.400254
  • Existe una correlación positiva moderada (0.40) entre 'velocity_4w' y 'velocity_6h'.